import {
    ArrayLiteral,
    BlockStatement,
    BooleanLiteral,
    CallExpression,
    ExpressionStatement,
    FunctionLiteral,
    HashLiteral,
    Identifier,
    IfExpression,
    IndexExpression,
    InfixExpression,
    IntegerLiteral,
    LetStatement,
    PrefixExpression,
    Program,
    ReturnStatement,
    StringLiteral
} from "./ast.js";
import { builtins } from "./builtins.js";
import {
    MonkeyArray,
    MonkeyBoolean,
    MonkeyBuiltin,
    MonkeyEnvironment,
    MonkeyError,
    MonkeyFunction,
    MonkeyHash,
    MonkeyInteger,
    MonkeyNull,
    MonkeyObjectType,
    MonkeyReturnValue,
    MonkeyString
} from "./object.js";
import { TokenType } from "./token.js";

const singleTrue = new MonkeyBoolean(true);
const singleFalse = new MonkeyBoolean(false);
const singleNull = new MonkeyNull();

function nativeBooleanToObject(bool) {
    return bool ? singleTrue : singleFalse;
}

export function newMonkeyError(msg) {
    return new MonkeyError(msg);
}

function isError(obj) {
    return obj && obj.type() === MonkeyObjectType.ERROR;
}

export function newEnvironment() {
    return new MonkeyEnvironment();
}

function newEnclosedEnvironment(outer) {
    return new MonkeyEnvironment(outer);
}

function evalBlockStatement(statements, env) {
    let result;
    for (const statement of statements) {
        result = evalMonkey(statement, env);
        const type = result.type();
        if (result && (type === MonkeyObjectType.RETURN_VALUE || type === MonkeyObjectType.ERROR)) {
            return result;
        }
    }
    return result;
}

function evalProgram(program, env) {
    let result
    for (const statement of program) {
        result = evalMonkey(statement, env);
        if (result instanceof MonkeyError) {
            return result;
        } else if (result instanceof MonkeyReturnValue) {
            return result.value;
        }
    }
    return result;
}

function evalBangOperatorExpression(right) {
    switch (right.value) {
        case true:
            return singleFalse;
        case false:
            return singleTrue;
        case null:
            return singleTrue;
        case undefined:
            return singleTrue;
        default:
            return singleFalse;
    }
}

function evalMinusPrefixOperatorExpression(right) {
    if (right.type() !== MonkeyObjectType.INTEGER) {
        return newMonkeyError(`unknown operator: -${right.type()}`);
    }
    const value = right.value;
    return new MonkeyInteger(-value);
}

function evalPrefixExpression(operator, right) {
    switch (operator) {
        case TokenType.BANG:
            return evalBangOperatorExpression(right);
        case TokenType.MINUS:
            return evalMinusPrefixOperatorExpression(right);
        default:
            return newMonkeyError(`unknown operator: ${operator}${right.type()}`);
    }
}

function evalIntegerInfixExpression(operator, left, right) {
    const leftVal = left.value;
    const rightVal = right.value;

    switch (operator) {
        case TokenType.PLUS:
            return new MonkeyInteger(leftVal + rightVal);
        case TokenType.MINUS:
            return new MonkeyInteger(leftVal - rightVal);
        case TokenType.ASTERISK:
            return new MonkeyInteger(leftVal * rightVal);
        case TokenType.SLASH:
            return new MonkeyInteger(leftVal / rightVal);
        case TokenType.LT:
            return nativeBooleanToObject(leftVal < rightVal);
        case TokenType.GT:
            return nativeBooleanToObject(leftVal > rightVal);
        case TokenType.EQ:
            return nativeBooleanToObject(leftVal === rightVal);
        case TokenType.NOT_EQ:
            return nativeBooleanToObject(leftVal !== rightVal);
        default:
            return newMonkeyError(`type mismatch: ${left.type()} ${operator} ${right.type()}`);
    }
}
function evalStringInfixExpression(operator, left, right) {
    if (operator !== TokenType.PLUS) {
        return new MonkeyError(
            `unknown operator: ${left.type()} ${operator} ${right.type()}`
        );
    }
    const leftVal = left.value;
    const rightVal = right.value;
    return new MonkeyString(leftVal + rightVal);
}
function evalInfixExpression(operator, left, right) {
    if (
        left.type() === MonkeyObjectType.INTEGER &&
        right.type() === MonkeyObjectType.INTEGER
    ) {
        return evalIntegerInfixExpression(operator, left, right);
    }
    if (
        left.type() === MonkeyObjectType.STRING &&
        right.type() === MonkeyObjectType.STRING
    ) {
        return evalStringInfixExpression(operator, left, right);
    }

    switch (true) {
        case operator === TokenType.EQ:
            return nativeBooleanToObject(left.value === right.value);
        case operator === TokenType.NOT_EQ:
            return nativeBooleanToObject(left.value !== right.value);
        case left.type !== right.type:
            return newMonkeyError(`type mismatch: ${left.type()} ${operator} ${right.type()}`)
        default:
            return newMonkeyError(`unknown operator: ${left.type()} ${operator} ${right.type()}`);
    }
}


function isTruthy(obj) {
    if (obj instanceof MonkeyInteger && obj.value !== 0) {
        return true
    }

    switch (obj) {
        case singleTrue:
            return true
        case singleNull:
        case singleFalse:
        default:
            return false
    }
}

function evalIfExpression(ifExpression, env) {
    const condition = evalMonkey(ifExpression.condition, env);

    if (isError(condition)) {
        return condition;
    }

    if (isTruthy(condition)) {
        return evalMonkey(ifExpression.consequence, env);
    } else if (ifExpression.alternative && ifExpression.alternative.token) {
        return evalMonkey(ifExpression.alternative, env)
    } else {
        return singleNull;
    }
}

function evalIdentifier(node, env) {
    const val = env.get(node?.value);
    if (val) {
        return val;
    }
    const builtin = builtins[node?.value];
    if (builtin) {
        return builtin;
    }
    return newMonkeyError(`identifier not found: ${node?.value}`);
}

function evalExpressions(expressions, env) {
    const result = [];
    for (const expression of expressions) {
        const evaluated = evalMonkey(expression, env);
        if (isError(evaluated)) {
            return [evaluated];
        }
        result.push(evaluated);
    }
    return result;
}

function extendedFunctionEnv(fn, args) {
    const env = newEnclosedEnvironment(fn.env);
    for (let paramIdx = 0; paramIdx < fn.parameters.length; paramIdx++) {
        env.set(fn.parameters[paramIdx].value, args[paramIdx]);
    }
    return env;
}

function unwrapReturnValue(obj) {
    if (obj instanceof MonkeyReturnValue) {
        return obj.value;
    }
    return obj;
}

function applyFunction(fnObj, args) {
    switch (true) {
        case fnObj instanceof MonkeyFunction: {
            const extendedEnv = extendedFunctionEnv(fnObj, args);
            const evaluated = evalMonkey(fnObj.body, extendedEnv);
            return unwrapReturnValue(evaluated);
        }
        case fnObj instanceof MonkeyBuiltin: {
            return fnObj.fn(...args);
        }
        default:
            return MonkeyError(`not a function: ${fn.type()}`)
    }
}

function evalArrayIndexExpression(array, index) {
    const arrayObjedt = array;
    const idx = index.value;
    const max = arrayObjedt.elements.length - 1;
    if (idx < 0 || idx > max) {
        return singleNull;
    }
    return arrayObjedt.elements[idx];
}

function evalIndexExpression(left, index) {
    if (left.type() === MonkeyObjectType.ARRAY && index.type() === MonkeyObjectType.INTEGER) {
        return evalArrayIndexExpression(left, index);
    } else if(left.type() === MonkeyObjectType.HASH) {
        return evalHashIndexExpression(left, index);
    } else {
        return newMonkeyError(`index operator not supported: ${left.type()}`)
    }
}

function evalHashIndexExpression(hash, index) {
    const hashObject = hash;
    if (!(
        index instanceof MonkeyString ||
        index instanceof MonkeyBoolean ||
        index instanceof MonkeyInteger
    )) {
        return newMonkeyError(`unusable as hash key: ${index.type()}`)
    }
    const key = index;
    const pair = hashObject.pairs[key.hashKey()];
    if (!pair) {
        return singleNull;
    }
    return pair;
}

function evalHashLiteral(node, env) {
    const pairs = {};
    for (let [keyNode, valueNode] of node.pairs) {
        const key = evalMonkey(keyNode, env);
        if (isError(key)) {
            return key;
        }
        const value = evalMonkey(valueNode, env);
        if (isError(value)) {
            return value;
        }
        let hashed = key.hashKey();
        pairs[hashed] = value;
    }
    return new MonkeyHash(pairs);
}

export function evalMonkey(node, env) {
    switch (true) {
        case node instanceof Program:
            return evalProgram(node.statements, env);
        case node instanceof ExpressionStatement:
            return evalMonkey(node.expression, env);
        case node instanceof IntegerLiteral:
            return new MonkeyInteger(node.value);
        case node instanceof BooleanLiteral:
            return nativeBooleanToObject(node.value);
        case node instanceof PrefixExpression: {
            const right = evalMonkey(node.right, env);
            if (isError(right)) {
                return right;
            }
            return evalPrefixExpression(node.operator, right);
        }
        case node instanceof InfixExpression: {
            const left = evalMonkey(node.left, env);
            if (isError(left)) {
                return left;
            }
            const right = evalMonkey(node.right, env);
            if (isError(right)) {
                return right;
            }
            return evalInfixExpression(node.operator, left, right);
        }
        case node instanceof BlockStatement:
            return evalBlockStatement(node.statements, env);
        case node instanceof IfExpression:
            return evalIfExpression(node, env);
        case node instanceof ReturnStatement: {
            const val = evalMonkey(node.returnValue, env);
            if (isError(val)) {
                return val;
            }
            return new MonkeyReturnValue(val);
        }
        case node instanceof LetStatement: {
            const val = evalMonkey(node.value, env);
            if (isError(val)) {
                return val;
            }
            env.set(node.name.value, val);
            return val;
        }
        case node instanceof Identifier: {
            return evalIdentifier(node, env);
        }
        case node instanceof FunctionLiteral: {
            const params = node.parameters;
            const body = node.body;
            return new MonkeyFunction(params, body, env);
        }
        case node instanceof CallExpression: {
            const functionObj = evalMonkey(node.function, env);
            if (isError(functionObj)) {
                return functionObj;
            }
            const args = evalExpressions(node.arguments, env);
            if (args.length === 1 && isError(args[0])) {
                return args[0];
            }
            return applyFunction(functionObj, args);
        }
        case node instanceof StringLiteral: {
            return new MonkeyString(node.value);
        }
        case node instanceof ArrayLiteral: {
            const elements = evalExpressions(node.elements, env);
            if (elements.length === 1 && isError(elements[0])) {
                return elements[0];
            }
            return new MonkeyArray(elements);
        }
        case node instanceof IndexExpression: {
            const left = evalMonkey(node.left, env);
            if (isError(left)) {
                return left;
            }
            const index = evalMonkey(node.index, env);
            if (isError(index)) {
                return index;
            }
            return evalIndexExpression(left, index);
        }
        case node instanceof HashLiteral:
            return evalHashLiteral(node, env);
    }
    return singleNull;
}